package ch.jamme.model;

import java.lang.reflect.*;
import java.util.*;
import java.util.Map.*;

import ch.jamme.util.*;

public abstract class AbstractDomainObject implements DomainObject {

	private Long id;
	
	public final Long getId() {
		return id;
	}

	public final void setId(Long id) {
		this.id = id;
	}
	
	@Override
	public String toString() {		
		return getClass().getSimpleName().toLowerCase() + "[" + getId() + "]";
	}

	@Override
	public boolean equals(Object obj) {
		if (!getClass().equals(obj.getClass())) return false;
		return getId().equals(((AbstractDomainObject)obj).getId());
	}

	@Override
	public int hashCode() {
		return toString().hashCode();
	}

	public int compareTo(DomainObject o) {
		return id.compareTo(o.getId());
	}
	
	// ////////////////////////////////////////////////////////////////////////
	// /// DeepEquals
	// ////////////////////////////////////////////////////////////////////////
	
	public final boolean deepEquals(AbstractDomainObject dObj) {
		return deepEquals(this, dObj, new ArrayList<AbstractDomainObject>());
	}

	private final boolean deepEquals(AbstractDomainObject dObj1, AbstractDomainObject dObj2, List<AbstractDomainObject> checkedObjects) {
		if (checkedObjects.contains(dObj1)) return true;
		checkedObjects.add(dObj1);

		if (!dObj1.equals(dObj2)) return false;
		
		for (Field field : IntrospectionUtil.getAllFields(dObj1.getClass())) {
			if (!IntrospectionUtil.makeAccesible(field)) continue;
			try {
				Object value1 = field.get(dObj1);
				Object value2 = field.get(dObj2);
				if (Logger.isDebugEnabledFor(this))
					Logger.debug(this, "Field " + field + " has value " + value1 + " for " + dObj1 + " and value " + value2 + " for " + dObj2);
				if (!areEqual(value1, value2, checkedObjects)) return false;
			} catch (Exception e) {
				throw new RuntimeException("Error on field: " + field, e);
			}
		}
		
		return true;
	}
	
	@SuppressWarnings("unchecked")
	private boolean areEqual(Object obj1, Object obj2, List<AbstractDomainObject> checkedObjects) {
		if (obj1 == null && obj2 == null) return true;
		if (obj1 == null || obj2 == null) return false;
		
		if (obj1 instanceof AbstractDomainObject) {
			return deepEquals((AbstractDomainObject)obj1, (AbstractDomainObject)obj2, checkedObjects);
		}

		if (obj1 instanceof Map) {
			Map<?,?> map1 = (Map<?,?>)obj1;
			Map<?,?> map2 = (Map<?,?>)obj2;
			if (map1.size() != map2.size()) return false;
			for (Entry<?,?> entry1 : map1.entrySet()) {
				Object key1 = entry1.getKey();
				Object value1 = entry1.getValue();
				Object value2 = map2.get(key1);
				boolean equal = value1 instanceof AbstractDomainObject ? 
						deepEquals((AbstractDomainObject)value1, (AbstractDomainObject)value2, checkedObjects) : 
						areEqual(value1, value2, checkedObjects);
				if (!equal) return false;
			}
			
			return true;
		}
		
		boolean isList = obj1 instanceof Iterable;
		boolean isArray = obj1.getClass().isArray();
		if (isList || isArray) {
			List<?> sortedList1 = null;
			List<?> sortedList2 = null;
			
			if (isArray) {
				sortedList1 = iterableToSortedList((Iterable)Arrays.asList((Object[])obj1));
				sortedList2 = iterableToSortedList((Iterable)Arrays.asList((Object[])obj2));
			}
			if (isList) {
				Iterator iterator1 = ((Iterable)obj1).iterator();
				if (iterator1.hasNext() && !(iterator1.next() instanceof Comparable)) {
					sortedList1 = (List)obj1;
					sortedList2 = (List)obj2;
				} else {
					sortedList1 = iterableToSortedList((Iterable)obj1);
					sortedList2 = iterableToSortedList((Iterable)obj2);
				}
			}
			if (sortedList1.size() != sortedList2.size()) return false;
			
			Iterator<?> iter1 = sortedList1.iterator();
			Iterator<?> iter2 = sortedList2.iterator();
			while (iter1.hasNext() && iter2.hasNext()) {
				Object item1 = iter1.next();
				Object item2 = iter2.next();
				boolean equal = item1 instanceof AbstractDomainObject ? 
						deepEquals((AbstractDomainObject)item1, (AbstractDomainObject)item2, checkedObjects) : 
						areEqual(item1, item2, checkedObjects);
				if (!equal) return false;
			}
			
			return true;
		}

		return areEqual(obj1, obj2);
	}
	
	private boolean areEqual(Object obj1, Object obj2) {
		if (obj1 instanceof byte[]) {
			return ((byte[])obj1).length == ((byte[])obj2).length;
		}
		
		if (obj1 instanceof java.util.Date) {
			return ((java.util.Date)obj1).getTime() == ((java.util.Date)obj2).getTime();
		}
		
		if (obj1 instanceof String) {
			String string1 = ((String)obj1).replaceAll("\\W", "");
			String string2 = ((String)obj2).replaceAll("\\W", "");
			return string1.equals(string2);
		}
		
		boolean areEqual = obj1.equals(obj2);
		if (Logger.isDebugEnabledFor(this))
			Logger.debug(this, obj1 + " and " + obj2 + " are " + (areEqual ? "" : " NOT ") + " equal");
		return areEqual;
	}

	private <T extends Comparable<? super T>> List<T> iterableToSortedList(Iterable<T> iterable) {
		List<T> sortedList = new ArrayList<T>();
		for (T o : iterable) sortedList.add(o);
		Collections.sort(sortedList);
		return sortedList;
	}
}
